package com.bestv.search.engine.command;

import com.bestv.search.common.util.QueryStringProcess;
import com.bestv.search.engine.cache.KeySubjectFirstLevelCache;
import com.bestv.search.engine.cache.KeySubjectFirstLevelCache.EntityValue;
import net.paoding.analysis.analyzer.PaodingAnalyzer;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.WhitespaceAnalyzer;
import org.apache.lucene.analysis.cn.ChineseAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.FieldSelector;
import org.apache.lucene.document.FieldSelectorResult;
import org.apache.lucene.index.CorruptIndexException;
import org.apache.lucene.index.Term;
import org.apache.lucene.queryParser.MultiFieldQueryParser;
import org.apache.lucene.queryParser.ParseException;
import org.apache.lucene.queryParser.QueryParser;
import org.apache.lucene.search.*;

import java.io.IOException;
import java.text.CollationKey;
import java.text.Collator;
import java.util.*;

public class LuceneSearchCommandForWS {

    protected final Log log = LogFactory.getLog(getClass());
    private String sortType;

    public String getSortType() {
        return sortType;
    }

    public void setSortType(String sortType) {
        this.sortType = sortType;
    }

    public String searchInOneFieldAndPtypeByOneKeyword(String ptype, String field, String keyword, final int start,
                                                       final int limit) {
        log.debug(ptype);
        field = QueryStringProcess.replaceAllIllegleChar(field);
        keyword = QueryStringProcess.replaceAllIllegleChar(keyword);
        BooleanQuery bq = new BooleanQuery();
        QueryParser qp = new QueryParser(field, new ChineseAnalyzer());
        Query kq = null;
        try {
            kq = qp.parse(keyword);
        } catch (ParseException e) {
            log.error(e);
        }
        if (kq != null)
            bq.add(kq, BooleanClause.Occur.MUST);
        if (!"all".equals(ptype)) {
            Query tq = new TermQuery(new Term("p_type", ptype));
            bq.add(tq, BooleanClause.Occur.MUST);
        }
        log.debug(bq);

        TopFieldDocs td = null;
        int totalHits = 0;
        if ("time".equals(getSortType())) {
            Sort sort = new Sort(new SortField("updateTimeMillis", SortField.LONG, true));
            try {
                td = LuceneSearchCommandRefactor.getIndexSearcher().search(bq, null, start + limit, sort);
                totalHits = td.totalHits;
            } catch (IOException e) {
                log.error(e);
            }
        } else if ("score".equals(getSortType())) {
        }

        ScoreDoc[] sds;
        if (td == null) {
            TopDocCollector tp = new TopDocCollector(start + limit);
            totalHits = tp.getTotalHits();
            try {
                LuceneSearchCommandRefactor.getIndexSearcher().search(bq, tp);
            } catch (IOException e) {
                log.error(e);
            }
            sds = tp.topDocs().scoreDocs;
        } else
            sds = td.scoreDocs;
        log.debug("sds.length: " + sds.length);
        if (sds.length <= 0)
            return "";
        int max = start + limit > sds.length ? sds.length : start + limit;
        StringBuffer sb = new StringBuffer("(");
        for (int i = start; i < max; ++i) {
            Document doc = null;
            try {
                doc = LuceneSearchCommandRefactor.indexSearcher.doc(sds[i].doc, new FieldSelector() {
                    private static final long serialVersionUID = 572974346658357281L;

                    public FieldSelectorResult accept(String fieldName) {
                        if ("id".equals(fieldName))
                            return FieldSelectorResult.LOAD;
                        return FieldSelectorResult.NO_LOAD;
                    }
                });
            } catch (CorruptIndexException e) {
                log.error(e);
            } catch (IOException e) {
                log.error(e);
            }
            if (doc != null)
                sb.append(doc.get("id")).append(",");
        }
        sb.deleteCharAt(sb.length() - 1);
        sb.append(")").append("&").append(totalHits);
        log.debug(sb.toString());
        return sb.toString();
    }

    /**
     * Accessed by Search Manager.
     *
     * @param keywords   Not null
     * @param field      May ""
     * @param bizTypes   May ""
     * @param type       May ""
     * @param updateTime time period
     * @param start      start index
     * @param limit      doc count
     * @return (ids of infos)&totalCount
     */
    @SuppressWarnings("unchecked")
    public String searchPreview(String keywords, String field, String bizTypes, String type, String updateTime,
                                int start, int limit) {
        log.debug("ptype = " + bizTypes);
        log.debug("type = " + type);
        log.debug("updateTime = " + updateTime);
        String[] keys = keywords.split("[ ]+");
        String[] ptypes = null;
        if (!"".equals(bizTypes))
            ptypes = bizTypes.split("[ ]+");
        Set<EntityValue> keySubjects = new HashSet<EntityValue>();
        if (ptypes == null) {
            Set<EntityValue> evs = null;
            for (String key : keys)
                evs = KeySubjectFirstLevelCache.getEntityFromHeap(key);
            if (evs != null)
                keySubjects.addAll(evs);
        } else {
            for (String ptype : ptypes) {
                for (String key : keys) {
                    EntityValue ev = KeySubjectFirstLevelCache.getEntityFromHeap(key, ptype);
                    if (ev != null)
                        keySubjects.add(ev);
                }
            }
        }
        try {
            keywords = QueryStringProcess.replaceAllIllegleChar(keywords);
            field = QueryStringProcess.replaceAllIllegleChar(field);
            bizTypes = QueryStringProcess.replaceAllIllegleChar(bizTypes);
            type = QueryStringProcess.replaceAllIllegleChar(type);
            Query[] q = buildQueryForSearchPreview(keySubjects, keywords, field, bizTypes, type, updateTime);
            log.debug("finish buildQueryForSearchPreview ...");
            log.debug("q[0]=" + q[0].toString());
            log.debug("q[1] = " + (q[1] == null ? "null" : q[1].toString()));
            Object[] obj_0 = searchPreview(q[0], start, limit);
            Object[] obj_1;
            int totalCount_0 = (Integer) obj_0[0];
            List<Document> docs_0 = (List<Document>) obj_0[1];
            log.debug("totalCount_0 = " + totalCount_0);
            int totalCount_1 = 0;
            List<Document> docs_1 = null;
            if (totalCount_0 <= start) {// The number of result searched by
                // query:q[0] is too small to even get 1
                // doc. All the docs need to fetch from
                // q[1]
                if (q[1] != null) {
                    start -= totalCount_0;
                    obj_1 = searchPreview(q[1], start, limit);
                    totalCount_1 = (Integer) obj_1[0];
                    docs_1 = (List<Document>) obj_1[1];
                }
                log.debug("totalCount_1 = " + totalCount_1);

                if (totalCount_0 + totalCount_1 == 0)
                    return "noDoc";
                if (docs_1 == null)
                    return "out of index";
                StringBuffer sb = new StringBuffer("(");
                for (Document doc : docs_1)
                    sb.append(doc.get("id")).append(",");
                sb.deleteCharAt(sb.length() - 1);
                sb.append(")&").append(totalCount_0 + totalCount_1);
                return sb.toString();
            }
            if (totalCount_0 < start + limit) {// some docs come from q[0],some
                // docs from q[1]
                if (q[1] != null) {
                    obj_1 = searchPreview(q[1], 0, limit - docs_0.size());
                    totalCount_1 = (Integer) obj_1[0];
                    docs_1 = (List<Document>) obj_1[1];
                }
                log.debug("totalCount_1 = " + totalCount_1);
                int totalCount = totalCount_0 + totalCount_1;
                if (totalCount <= 0)
                    return "noDoc";
                StringBuffer sb = new StringBuffer("(");
                for (Document doc : docs_0)
                    sb.append(doc.get("id")).append(",");
                if (docs_1 != null)
                    for (Document doc : docs_1)
                        sb.append(doc.get("id")).append(",");
                sb.deleteCharAt(sb.length() - 1);
                sb.append(")&").append(totalCount);
                return sb.toString();
            }
            // Search by query:q[0] is big enough, just return
            // doc[start,....start + limit -1]
            if (docs_0.size() <= 0)
                return "noDoc";
            StringBuffer sb = new StringBuffer("(");
            for (Document doc : docs_0)
                sb.append(doc.get("id")).append(",");
            sb.deleteCharAt(sb.length() - 1);
            sb.append(")&").append(totalCount_0);
            return sb.toString();
        } catch (ParseException e) {
            log.error(e);
            return "Parser Error";
        } catch (IOException e) {
            log.error(e);
            return "Search Error! IO Exception.";
        }
    }

    public String searchInType(String ptype, String keywords, String must, final int start, final int limit,
                               final boolean isIn) throws ParseException {
        keywords = QueryStringProcess.replaceAllIllegleChar(keywords);
        if (!must.matches("[ ]*"))
            must = QueryStringProcess.replaceAllIllegleChar(must);
        BooleanQuery.setMaxClauseCount(Integer.MAX_VALUE);
        BooleanQuery mustQuery = null;
        if (!must.matches("[ ]*")) {
            MultiFieldQueryParser mustParser = new MultiFieldQueryParser(LuceneSearchCommand
                    .getSingletonLuceneSearchCommand().getSearchFields(), new ChineseAnalyzer(), LuceneSearchCommand
                    .getSingletonLuceneSearchCommand().getFieldBoosts());
            mustQuery = new BooleanQuery();
            String[] musts = must.split("[ ]");
            for (String s : musts) {
                mustQuery.add(mustParser.parse(s), BooleanClause.Occur.MUST);
            }
        }
        QueryParser qp = new QueryParser("keyword", new ChineseAnalyzer());
        Query keyQuery = qp.parse(QueryStringProcess.replaceAllIllegleChar(keywords));
        BooleanQuery bool = new BooleanQuery();
        bool.add(keyQuery, BooleanClause.Occur.MUST);
        if (mustQuery != null)
            bool.add(mustQuery, BooleanClause.Occur.MUST);
        Query ptQuery = new TermQuery(new Term("p_type", ptype));
        if (isIn)
            bool.add(ptQuery, BooleanClause.Occur.MUST);
        else
            bool.add(ptQuery, BooleanClause.Occur.MUST_NOT);
        log.debug(bool.toString());
        TopDocCollector tp = null;
        TopFieldDocs td;
        ScoreDoc[] sds = new ScoreDoc[0];
        // 是否按时间排序
        if (!"time".equals(getSortType()))
            tp = new TopDocCollector(start + limit);
        try {
            if (tp != null) {
                LuceneSearchCommand.getIndexSearcher().search(bool, tp);
                sds = tp.topDocs().scoreDocs;
            } else {
                Sort sort = new Sort(new SortField("updateTimeMillis", SortField.LONG, true));
                td = LuceneSearchCommand.getIndexSearcher().search(bool, null, start + limit, sort);
                sds = td.scoreDocs;
            }
        } catch (IOException e) {
            log.error(e);
        }
        log.debug("sds.length: " + sds.length);
        if (sds.length <= 0)
            return "[noDoc]";
        int max = start + limit > sds.length ? sds.length : start + limit;
        StringBuffer sb = new StringBuffer("(");
        for (int i = start; i < max; ++i) {
            Document doc;
            try {
                doc = LuceneSearchCommand.indexSearcher.doc(sds[i].doc, new FieldSelector() {
                    private static final long serialVersionUID = 6181014324595316371L;

                    public FieldSelectorResult accept(String fieldName) {
                        if ("id".equals(fieldName))
                            return FieldSelectorResult.LOAD;
                        return FieldSelectorResult.NO_LOAD;
                    }
                });
                sb.append(doc.get("id")).append(",");
            } catch (CorruptIndexException e) {
                log.error(e);
            } catch (IOException e) {
                log.error(e);
            }
        }
        sb.deleteCharAt(sb.length() - 1);
        sb.append(")").append("&").append(tp != null ? tp.getTotalHits() : 0);
        return sb.toString();
    }

    private Object[] searchPreview(Query q, int start, int limit) throws IOException {
        Object[] obj = new Object[2];
        List<Document> docs = new ArrayList<Document>();
        TopDocCollector topDocs = null;
        TopFieldDocs td;
        log.debug("sort type: " + getSortType());
        if (!"time".equals(getSortType())) {
            topDocs = new TopDocCollector(start + limit);
        }
        IndexSearcher is = LuceneSearchCommand.getIndexSearcher();
        for (int i = 0; i < 5; ++i) {
            if (is != null)
                break;
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                log.error(e);
            }
        }
        int totalCount = 0;
        ScoreDoc[] sds = null;
        if (topDocs != null) {
            log.debug("topDocs != null");
            if (is != null)
                is.search(q, topDocs);
            totalCount = topDocs.getTotalHits();
            sds = topDocs.topDocs().scoreDocs;
        } else {
            log.debug("topDocs == null");
            Sort sort = new Sort(new SortField("updateTimeMillis", SortField.LONG, true));
            if (is != null) {
                td = is.search(q, null, start + limit, sort);
                totalCount = td.totalHits;
                sds = td.scoreDocs;
            }
        }
        obj[0] = totalCount;
        if (start >= totalCount)
            return obj;
        int max = start + limit > totalCount ? totalCount : start + limit;
        if (is != null) {
            for (int k = start; k < max; ++k) {
                Document doc = is.doc(sds[k].doc, new FieldSelector() {
                    private static final long serialVersionUID = -8008091486091805061L;

                    public FieldSelectorResult accept(String fieldName) {
                        if ("id".equals(fieldName))
                            return FieldSelectorResult.LOAD;
                        return FieldSelectorResult.NO_LOAD;
                    }
                });
                docs.add(doc);
            }
        }
        obj[1] = docs;
        return obj;
    }

    /**
     * @param keySubjects Not null, but can be empty
     * @param keywords    Not null
     * @param field       May ""
     * @param bizTypes    May ""
     * @param types       May ""
     * @param updateTime  time period
     * @return the query to be searched
     * @throws ParseException parse exception
     */
    private Query[] buildQueryForSearchPreview(Set<EntityValue> keySubjects, String keywords, String field,
                                               String bizTypes, String types, String updateTime) throws ParseException {
        keywords = QueryStringProcess.replaceAllIllegleChar(keywords);
        field = QueryStringProcess.replaceAllIllegleChar(field);
        bizTypes = QueryStringProcess.replaceAllIllegleChar(bizTypes);
        types = QueryStringProcess.replaceAllIllegleChar(types);
        log.debug("buildQueryForSearchPreview....");
        BooleanQuery.setMaxClauseCount(Integer.MAX_VALUE);
        boolean isBizTypeNotNull = (!"".equals(bizTypes));
        BooleanQuery[] bools = new BooleanQuery[]{new BooleanQuery(), null};
        if (isBizTypeNotNull)
            bools[1] = new BooleanQuery();
        // Analyzer sal = new StandardAnalyzer();
        Analyzer sal = new PaodingAnalyzer();
        String[] fields = field.split("[ ]+");
        Query typeQuery = null;
        MultiFieldQueryParser chineseQp;
        if (!"".equals(field))
            chineseQp = new MultiFieldQueryParser(fields, sal, getFieldBoosts(fields));
        else
            chineseQp = new MultiFieldQueryParser(LuceneSearchCommandRefactor.searchFields, sal,
                    LuceneSearchCommandRefactor.getFieldsBoost());
        if (!"".equals(types)) {
            QueryParser typeQueryParser = new QueryParser("type", new WhitespaceAnalyzer());
            typeQuery = typeQueryParser.parse(types);
        }
        Query timeRange = null;
        if (!"".equals(updateTime)) {
            String[] uts = updateTime.split("TO");
            if (uts.length == 2) {
                Term utf = "min".equals(uts[0].trim()) ? null : new Term("updateTimeMillis", uts[0].trim());
                Term utt = "max".equals(uts[1].trim()) ? null : new Term("updateTimeMillis", uts[1].trim());
                if (utf != null || utt != null)
                    timeRange = new RangeQuery(utf, utt, true, new Collator() {
                        public int compare(String source, String target) {
                            long s = Long.valueOf(source);
                            long t = Long.valueOf(target);
                            return s > t ? 1 : (s == t ? 0 : -1);
                        }

                        public CollationKey getCollationKey(String source) {
                            return null;
                        }

                        public int hashCode() {
                            return 0;
                        }
                    });
            }
        }
        // deal with KeySubjects in cache
        if (keySubjects.size() > 0) {
            Iterator<EntityValue> it = keySubjects.iterator();
            while (it.hasNext()) {
                BooleanQuery[] bq = new BooleanQuery[]{new BooleanQuery(), null};
                if (isBizTypeNotNull)
                    bq[1] = new BooleanQuery();
                EntityValue ev = it.next();
                Query evKeyQuery = chineseQp.parse(QueryStringProcess.replaceAllIllegleChar(ev.getKeywords()));
                bq[0].add(evKeyQuery, BooleanClause.Occur.MUST);
                if (isBizTypeNotNull)
                    bq[1].add(evKeyQuery, BooleanClause.Occur.MUST);
                if (ev.getMust() != null && !"".equals(ev.getMust())) {
                    if (!ev.getMust().matches("[ ]*")) {
                        String[] musts = QueryStringProcess.replaceAllIllegleChar(ev.getMust()).split("[ ]+");
                        for (String must : musts) {
                            Query mustQuery = chineseQp.parse(must);
                            bq[0].add(mustQuery, BooleanClause.Occur.MUST);
                            if (isBizTypeNotNull)
                                bq[1].add(mustQuery, BooleanClause.Occur.MUST);
                        }
                    }
                }
                Query bizTypeQuery = new TermQuery(new Term("p_type", QueryStringProcess.replaceAllIllegleChar(ev
                        .getBizType())));
                bq[0].add(bizTypeQuery, BooleanClause.Occur.MUST);
                if (typeQuery != null)
                    bq[0].add(typeQuery, BooleanClause.Occur.MUST);
                if (timeRange != null)
                    bq[0].add(timeRange, BooleanClause.Occur.MUST);
                bools[0].add(bq[0], BooleanClause.Occur.SHOULD);
                if (isBizTypeNotNull) {
                    bq[1].add(bizTypeQuery, BooleanClause.Occur.MUST_NOT);
                    if (typeQuery != null)
                        bq[1].add(typeQuery, BooleanClause.Occur.MUST);
                    if (timeRange != null)
                        bq[1].add(timeRange, BooleanClause.Occur.MUST);
                    bools[1].add(bq[1], BooleanClause.Occur.SHOULD);
                }
            }
        }
        // deal with other condition
        log.debug(keywords);
        Query keyQuery = chineseQp.parse(keywords);
        if (isBizTypeNotNull) {
            BooleanQuery[] bq = new BooleanQuery[]{new BooleanQuery(), new BooleanQuery()};
            bq[0].add(keyQuery, BooleanClause.Occur.MUST);
            bq[1].add(keyQuery, BooleanClause.Occur.MUST);
            QueryParser termParserQuery = new QueryParser("p_type", new WhitespaceAnalyzer());
            Query bizTypeQuery = termParserQuery.parse(bizTypes);
            bq[0].add(bizTypeQuery, BooleanClause.Occur.MUST);
            bq[1].add(bizTypeQuery, BooleanClause.Occur.MUST_NOT);
            if (typeQuery != null) {
                bq[0].add(typeQuery, BooleanClause.Occur.MUST);
                bq[1].add(typeQuery, BooleanClause.Occur.MUST);
            }
            if (timeRange != null) {
                bq[0].add(timeRange, BooleanClause.Occur.MUST);
                bq[1].add(timeRange, BooleanClause.Occur.MUST);
            }
            bools[0].add(bq[0], BooleanClause.Occur.SHOULD);
            bools[1].add(bq[1], BooleanClause.Occur.SHOULD);
        } else {
            if (typeQuery != null || timeRange != null) {
                BooleanQuery bq = new BooleanQuery();
                bq.add(keyQuery, BooleanClause.Occur.MUST);
                if (typeQuery != null)
                    bq.add(typeQuery, BooleanClause.Occur.MUST);
                if (timeRange != null)
                    bq.add(timeRange, BooleanClause.Occur.MUST);
                bools[0].add(bq, BooleanClause.Occur.SHOULD);
            } else
                bools[0].add(keyQuery, BooleanClause.Occur.SHOULD);
            // bools[1].add(keyQuery,BooleanClause.Occur.SHOULD);
        }
        return bools;
    }

    private Map<String, Float> getFieldBoosts(String[] fields) {
        Map<String, Float> fieldBoosts = new HashMap<String, Float>();
        Map<String, Float> map = LuceneSearchCommandRefactor.getFieldsBoost();
        Set<String> keys = map.keySet();
        for (String fd : fields) {
            if (keys.contains(fd))
                fieldBoosts.put(fd, map.get(fd));
        }
        return fieldBoosts;
    }
}
